Перейти к основному содержимому

5.02. Функции

Разработчику Архитектору

Функции

Что такое функция?

Функция — это именованный блок кода, предназначенный для выполнения определённой задачи. Она может принимать входные данные (аргументы), обрабатывать их и возвращать результат. Функции позволяют организовать код по принципу модульности: повторное использование, изоляция логики, упрощение отладки и тестирования.

В отличие от некоторых других языков программирования, таких как Java или C#, где методы строго связаны с классами и требуют явного указания типов возвращаемого значения и параметров, в Python функции являются автономными конструкциями, не привязанными к классам по умолчанию, а система типов — динамической. Это не означает, что типы отсутствуют, но они проверяются во время выполнения, а не на этапе компиляции.


Особенности функций в Python

Функции в Python обладают рядом характерных черт, отличающих их от аналогичных конструкций в строго типизированных языках:

  1. Динамическая типизация параметров и возвращаемых значений. Типы аргументов и возвращаемых значений не указываются в сигнатуре функции. Проверка корректности типов происходит только во время выполнения.
  2. Гибкая передача аргументов. Поддерживается комбинированное использование позиционных, именованных аргументов, а также распаковка коллекций через *args и словарей через **kwargs.
  3. Функции — объекты первого класса. Функции можно присваивать переменным, передавать как аргументы, возвращать из других функций. Эта особенность фундаментальна для понимания поведения функций в Python, даже если она будет рассмотрена подробнее позже.
  4. Отсутствие необходимости объявления типа возврата. Ключевое слово return используется для возврата значения, но тип возвращаемого результата не декларируется. Функция может возвращать разные типы в разных ветвях исполнения.
  5. Автоматическое создание локального пространства имён. При вызове функции создаётся новая область видимости, в которой существуют локальные переменные. Управление доступом к глобальным и нелокальным переменным осуществляется через явные инструкции.

Объявление функции

Функция в Python объявляется с помощью ключевого слова def, за которым следует имя функции, список параметров в круглых скобках и двоеточие.

Тело функции представляет собой блок кода, выделенный отступом (обычно 4 пробела).

def greet(name):
return f"Hello, {name}!"

Синтаксис:

def <имя_функции>(<список_параметров>):
<тело функции>

Имя функции должно следовать правилам идентификаторов: начинаться с буквы или подчёркивания, содержать только буквы, цифры и подчёркивания. Рекомендуется использовать змеиный_регистр (snake_case) согласно PEP 8.


Структура функции

Тело функции форматируется с соблюдением отступов. Отступ определяет принадлежность строк к блоку. Все строки с одинаковым отступом считаются частью одного блока. Завершение отступа означает выход из области действия функции.

Параметр — переменная, указанная в сигнатуре функции.

Аргумент — фактическое значение, переданное функции при вызове.

Например, в вызове greet("Alice"), "Alice" — аргумент, а name в def greet(name): — параметр.

Позиционные параметры. Аргументы передаются в порядке, соответствующем порядку параметров в определении функции.

def add(a, b):
return a + b

result = add(3, 5) # a=3, b=5

Именованные параметры (ключевые аргументы). Аргументы передаются с указанием имени параметра. Это позволяет задавать аргументы в произвольном порядке.

result = add(b=5, a=3)

Именованные аргументы могут использоваться только после позиционных.

Часто можно встретить такой подход:

print(4, end="")

Он означает, буквально "выведи 4" и добавляет в конец строки пустую строку "".

Параметр end="" — это именованный аргумент функции print() в Python, который определяет, чем завершается вывод строки.

По умолчанию:

print(x)  # эквивалентно print(x, end='\n')

— после вывода x добавляется символ перевода строки (\n), и следующий print() начнётся с новой строки.

Если задать end="":

print(x, end="")

— после x ничего не выводится, в том числе нет перевода строки. Следующий print() (или другой вывод) продолжит писать в той же строке.

Без end (по умолчанию):

print("A")
print("B")

Вывод:

A
B

С end="":

print("A", end="")
print("B", end="")
print("C")

Вывод:

ABC

Параметры со значениями по умолчанию. Параметру может быть присвоено значение по умолчанию. Такой параметр становится необязательным при вызове.

def greet(name, greeting="Hello"):
return f"{greeting}, {name}!"

print(greet("Bob")) # Hello, Bob!
print(greet("Bob", "Hi")) # Hi, Bob!

Параметры со значениями по умолчанию должны следовать после всех обязательных параметров. Значения по умолчанию вычисляются один раз при определении функции. Не рекомендуется использовать изменяемые объекты (например, списки или словари) в качестве значений по умолчанию.

Параметры до / — только позиционные.

Параметры после * — только именованные.

def func(pos_only, /, standard, *, kwd_only):
pass

Здесь:

  • pos_only можно передать только позиционно.
  • standard — и позиционно, и по имени.
  • kwd_only — только по имени.

Произвольное число аргументов

Python предоставляет механизм для приёма произвольного числа аргументов. • *args — собирает все позиционные аргументы в кортеж. • **kwargs — собирает все именованные аргументы в словарь.

def log_call(*args, **kwargs):
print(f"Позиционные аргументы: {args}")
print(f"Именованные аргументы: {kwargs}")

log_call(1, 2, action="save", user="admin")
# Вывод:
# Позиционные аргументы: (1, 2)
# Именованные аргументы: {'action': 'save', 'user': 'admin'}

Эти механизмы часто используются при создании декораторов, базовых классов, функций-обёрток.

Распаковка аргументов при вызове:

args = [1, 2]
kwargs = {"c": 3, "d": 4}

def func(a, b, c, d):
return a + b + c + d

func(*args, **kwargs) # эквивалентно func(1, 2, c=3, d=4)

Возврат значения

Оператор return завершает выполнение функции и возвращает указанное значение вызывающему коду. Если return отсутствует или не содержит значения, функция возвращает None.

def square(x):
return x ** 2

def do_nothing():
pass # вернёт None

result = do_nothing() # result == None

Кортеж

Функция может возвращать несколько значений — на практике это кортеж:

def divide_remainder(a, b):
return a // b, a % b

quotient, remainder = divide_remainder(10, 3)

Это синтаксический сахар для создания и распаковки кортежа.


Области видимости

В Python действует правило LEGB:

  • L — Local (локальная область — внутри функции),
  • E — Enclosing (объемлющая — вложенная функция),
  • G — Global (глобальная — модуль),
  • B — Built-in (встроенная — вроде print, len).

Переменные, объявленные внутри функции, по умолчанию являются локальными.

x = "global"

def outer():
x = "enclosing"

def inner():
x = "local"
print(x) # local

inner()
print(x) # enclosing

outer()
print(x) # global

Для изменения глобальной переменной внутри функции используется ключевое слово global:

counter = 0

def increment():
global counter
counter += 1

Для изменения переменной из объемлющей области — nonlocal:

def outer():
x = 1
def inner():
nonlocal x
x += 1
inner()
print(x) # 2

Без nonlocal интерпретатор создаст новую локальную переменную x, не затрагивая внешнюю.


Анонимные функции

Кроме именованных функций, объявляемых через def, Python поддерживает анонимные функции с помощью выражения lambda.

Синтаксис:

lambda <параметры>: <выражение>

Тело lambda — одно выражение (не блок кода). Не может содержать операторы (return, if, for и т.п.), кроме тернарного оператора. Не может иметь аннотаций типов (хотя это технически возможно, но не поддерживается стандартом).

Примеры:

square = lambda x: x ** 2
add = lambda a, b: a + b
is_even = lambda n: n % 2 == 0

lambda часто используется как аргумент для функций высшего порядка:

data = [(1, 'b'), (2, 'a'), (3, 'c')]
sorted(data, key=lambda x: x[1]) # сортировка по второму элементу

Хотя lambda удобна для кратких случаев, её использование не обязательно. Любая lambda-функция может быть заменена на обычную, определённую через def. Выбор зависит от читаемости и контекста.


Рекурсия

Функция называется рекурсивной, если она вызывает саму себя.

Рекурсия — естественный способ решения задач, имеющих рекурсивную структуру (например, обход деревьев, вычисление факториала, последовательности Фибоначчи). Пример:

def factorial(n):
if n <= 1:
return 1
return n * factorial(n - 1)

Базовый случай — условие завершения рекурсии. Без него возникает бесконечная рекурсия и переполнение стека вызовов.

Глубина рекурсии в Python ограничена (по умолчанию ~1000). Её можно изменить с помощью sys.setrecursionlimit(), но это не рекомендуется из-за риска аварийного завершения.

Рекурсия может быть менее эффективной, чем итерация, из-за накладных расходов на вызовы. Однако она часто упрощает реализацию алгоритмов на основе разделяй-и-властвуй.

Иногда требуется объявить функцию, тело которой пока не реализовано. Для этого используется оператор-заглушка pass.

def todo():
pass # заглушка, ничего не делает

Также можно использовать ... (Ellipsis), хотя это менее распространено:

def stub():
...

Благодаря динамической типизации, гибкой системе аргументов и возможности возвращать функции, они служат основой для многих парадигм программирования — от процедурной до функциональной.


Функции первого класса

Термин «объект первого класса» (first-class object) происходит из теории языков программирования и означает сущность, которая:

  • Может быть присвоена переменной.
  • Может быть передана как аргумент другой функции.
  • Может быть возвращена из функции.
  • Может быть сохранена в структурах данных (списках, словарях, множествах и т.п.).

Если такой объект — функция, то говорят: функция является объектом первого класса. В Python все функции удовлетворяют этим критериям. Это не просто синтаксическая конструкция — это полноценные объекты, существующие в runtime, обладающие типом, атрибутами и поведением, как и любые другие данные. Важно: термин «первого класса» не означает «лучше» или «высокопроизводительнее». Он указывает на степень интеграции функций в систему типов и выполнения программы.

Например, в C функции нельзя напрямую хранить в списках или возвращать из других функций без использования указателей (что выходит за рамки базовой модели языка). В Java до версии 8 методы не были объектами первого класса — их можно было использовать только через интерфейсы или рефлексию.

В Python же функция — полноценный объект, создаваемый во время исполнения.

Поскольку функции в Python — объекты, они могут использоваться так же, как числа, строки или списки.

Присваивание функции переменной:

def greet(name):
return f"Hello, {name}!"

say_hello = greet # Присваиваем функцию переменной
print(say_hello("Alice")) # Вызов через новое имя

Здесь greet и say_hello — два имени, ссылающиеся на один и тот же объект-функцию.

Функция, объявленная через ключевое слово def, создаёт объект и связывает его с именем. Это имя можно использовать для вызова функции, но сам объект функции существует независимо от имени:

def calculate_total(items):
return sum(items)

processor = calculate_total
result = processor([10, 20, 30]) # 60

Имена calculate_total и processor ссылаются на один и тот же объект. Изменение одного имени не влияет на доступность функции через другое имя. Удаление исходного имени через del calculate_total не уничтожает объект, пока на него существует хотя бы одна активная ссылка.

Функция может принимать другую функцию в качестве параметра. Такие функции называются функциями высшего порядка:

def transform_data(data, transformer):
return [transformer(item) for item in data]

def to_upper(text):
return text.upper()

words = ["python", "java", "csharp"]
result = transform_data(words, to_upper) # ['PYTHON', 'JAVA', 'CSHARP']

Стандартная библиотека Python активно использует этот подход. Функции map(), filter(), sorted() принимают функции для преобразования или сравнения элементов:

numbers = [3, 1, 4, 1, 5]
sorted_numbers = sorted(numbers, key=lambda x: -x) # [5, 4, 3, 1, 1]

Передача функции как аргумента:

def apply_operation(func, x):
return func(x)

def square(n):
return n ** 2

result = apply_operation(square, 5) # 25

Такой подход лежит в основе функций высшего порядка — функций, которые принимают или возвращают другие функции.

Хранение функций в структурах данных:

operations = {
'square': lambda x: x ** 2,
'double': lambda x: x * 2,
'negate': lambda x: -x
}

result = operations['square'](4) # 16

Это позволяет динамически выбирать поведение программы.


Атрибуты функций

Функция как объект имеет атрибуты, доступные во время выполнения:

def fetch_user(user_id):
"""Получает данные пользователя по идентификатору"""
return {"id": user_id, "name": "User"}

print(fetch_user.__name__) # 'fetch_user'
print(fetch_user.__doc__) # 'Получает данные пользователя по идентификатору'
print(fetch_user.__module__) # имя модуля, где определена функция

Пользовательские атрибуты можно добавлять динамически:

fetch_user.version = "1.2"
fetch_user.cache_ttl = 300
greet.author = "John Doe"
print(greet.author) # John Doe

Эти атрибуты используются фреймворками для хранения метаданных: права доступа, кэш-политики, версии API.

Функция — это объект типа function. Его можно исследовать:

print(type(greet))                    # <class 'function'>
print(callable(greet)) # True
print(greet.__name__) # 'greet'
print(greet.__module__) # имя модуля

Замыкание

Замыкание — это функция, которая «захватывает» переменные из своей объемлющей области видимости и продолжает иметь к ним доступ даже после завершения этой области.

def create_formatter(prefix):
def format_value(value):
return f"{prefix}: {value}"
return format_value

error_formatter = create_formatter("ERROR")
info_formatter = create_formatter("INFO")

print(error_formatter("File not found")) # ERROR: File not found
print(info_formatter("Process started")) # INFO: Process started

Функция format_value захватывает переменную prefix из области видимости create_formatter. После возврата format_value из create_formatter, переменная prefix сохраняется в специальной структуре, называемой ячейкой окружения (cell). Объект функции хранит ссылку на это окружение через атрибут __closure__:

print(error_formatter.__closure__[0].cell_contents)  # 'ERROR'

Условия возникновения замыкания - наличие вложенной функции; вложенная функция ссылается на переменную из внешней функции; внешняя функция возвращает внутреннюю.

def make_multiplier(factor):
def multiply(x):
return x * factor # захватывает factor
return multiply

double = make_multiplier(2)
triple = make_multiplier(3)

print(double(5)) # 10
print(triple(5)) # 15

Здесь multiply — замыкание. Оно «помнит» значение factor, даже когда make_multiplier уже завершилась.

Когда функция возвращается, вместе с ней возвращается и ссылка на её окружение (cell objects), в котором хранятся захваченные переменные. Это обеспечивает инкапсуляцию состояния без использования классов. Проверить наличие замыкания можно через атрибут __closure__:

print(double.__closure__)           # (<cell at 0x...: int object at 0x...>,)
print(double.__closure__[0].cell_contents) # 2

Замыкания позволяют инкапсулировать состояние без использования классов. Пример счётчика:

def create_counter(start=0):
count = start
def increment(step=1):
nonlocal count
count += step
return count
return increment

counter = create_counter(10)
print(counter()) # 11
print(counter(5)) # 16

Переменная count существует в окружении функции increment и сохраняет своё значение между вызовами. Ключевое слово nonlocal указывает, что переменная count берётся из ближайшей объемлющей области, а не создаётся как новая локальная переменная.

Другой пример — фабрика обработчиков событий с контекстом:

def make_event_handler(user_id):
def handle_event(event_type, data):
log_entry = {
"user_id": user_id,
"event_type": event_type,
"timestamp": get_current_time(),
"payload": data
}
save_to_audit_log(log_entry)
return handle_event

admin_handler = make_event_handler(1)
admin_handler("login", {"ip": "192.168.1.100"})

Каждый обработчик сохраняет свой user_id в замыкании, обеспечивая изоляцию контекста без передачи идентификатора при каждом вызове.


Декоратор

Декоратор — это функция, которая принимает другую функцию и возвращает новую функцию, расширяя или изменяя её поведение без модификации исходного кода. Декораторы реализуют принцип открытости/закрытости: объект (функция) открыт для расширения, но закрыт для изменения. Они позволяют вынести побочные эффекты (логирование, проверку прав, кэширование) в отдельные модули, улучшая читаемость и повторное использование.

Синтаксис @decorator. Без декоратора:

def my_function():
print("Основная логика")

def logger(func):
def wrapper():
print(f"Вызов: {func.__name__}")
func()
print("Вызов завершён")
return wrapper

my_function = logger(my_function)

С синтаксисом @:

@logger
def my_function():
print("Основная логика")

Оба варианта эквивалентны. Декоратор применяется при определении функции.

Декоратор с параметром — это функция, возвращающая декоратор. То есть трёхуровневая структура:

def repeat(times):
def decorator(func):
def wrapper(*args, **kwargs):
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator

@repeat(times=3)
def say_hello():
print("Привет!")

Здесь repeat(3) возвращает декоратор. Декоратор применяется к say_hello.


Примеры декораторов

Логирование вызовов:

def log_execution(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
try:
result = func(*args, **kwargs)
duration = time.time() - start
logger.info(f"{func.__name__} завершена за {duration:.4f} сек")
return result
except Exception as e:
duration = time.time() - start
logger.error(f"{func.__name__} завершена с ошибкой за {duration:.4f} сек: {e}")
raise
return wrapper

Кэширование результатов:

def memoize(func):
cache = {}
@wraps(func)
def wrapper(*args):
if args not in cache:
cache[args] = func(*args)
return cache[args]
return wrapper

@memoize
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)

Проверка прав доступа:

def require_role(role):
def decorator(func):
@wraps(func)
def wrapper(user, *args, **kwargs):
if user.get('role') != role:
raise PermissionError(f"Требуется роль {role}")
return func(user, *args, **kwargs)
return wrapper
return decorator

@require_role('admin')
def delete_user(user, target_id):
database.delete(target_id)

Декораторы с параметрами

Декоратор с параметрами представляет собой функцию, которая возвращает декоратор. Структура имеет три уровня вложенности:

def retry(max_attempts=3, delay=1):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
attempts = 0
while attempts < max_attempts:
try:
return func(*args, **kwargs)
except Exception as e:
attempts += 1
if attempts == max_attempts:
raise
time.sleep(delay)
logger.warning(f"Попытка {attempts} не удалась: {e}")
return wrapper
return decorator

@retry(max_attempts=5, delay=2)
def fetch_data(url):
response = requests.get(url)
response.raise_for_status()
return response.json()

Параметры max_attempts и delay передаются при применении декоратора. Функция retry возвращает конкретный декоратор, настроенный под заданные параметры. Этот декоратор затем применяется к функции fetch_data.


Сохранение метаданных функции

Декоратор заменяет оригинальную функцию обёрткой. Без дополнительных мер метаданные теряются:

print(compute.__name__)  # 'wrapper', а не 'compute'

Модуль functools предоставляет декоратор @wraps, который копирует метаданные из оригинальной функции в обёртку:

from functools import wraps

def log_calls(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"Вызов {func.__name__}")
return func(*args, **kwargs)
return wrapper

После применения @wraps атрибуты __name__, __doc__, __module__ сохраняют значения оригинальной функции.

Хранение функций в структурах данных

Функции можно помещать в списки, кортежи, словари и множества. Это позволяет создавать динамические наборы поведений:

operations = {
'add': lambda a, b: a + b,
'subtract': lambda a, b: a - b,
'multiply': lambda a, b: a * b
}

def execute(op_name, x, y):
return operations[op_name](x, y)

result = execute('multiply', 6, 7) # 42

Такой подход применяется в маршрутизаторах веб-фреймворков, обработчиках событий и системах плагинов. Каждый маршрут или событие связывается с функцией-обработчиком, которая вызывается при наступлении соответствующего условия.